﻿// Shader-Based-Image-Processing (SBIP)
// http://code.google.com/p/sbip/
//
// Copyright © Frank Nagl, 2009-2010
// admin@franknagl.de
//
namespace SBIP.Filter
{
    using System;
    using Microsoft.Xna.Framework.Graphics;
    using CommonAlgorithms = Grayscale.CommonAlgorithms;
    using Versions = FixedGaussianBlur.Versions;

    /// <summary>
    /// Canny edge detector.
    /// </summary>
    /// 
    /// <remarks><para>The filter searches for objects' edges by applying Canny edge detector.
    /// The implementation follows
    /// <a href="http://www.pages.drexel.edu/~weg22/can_tut.html">Bill Green's Canny edge detection tutorial</a>.</para>
    /// 
    /// <para><note>The implemented canny edge detector has two differences with the above linked algorithm.
    /// The first difference is in hysteresis step, which is a bit simplified (getting faster as a result). On the
    /// hysteresis step each pixel is compared with two threshold values: <see cref="HighThreshold"/> and
    /// <see cref="LowThreshold"/>. If pixel's value is greater or equal to <see cref="HighThreshold"/>, then
    /// it is kept as edge pixel. If pixel's value is greater or equal to <see cref="LowThreshold"/>, then
    /// it is kept as edge pixel only if there is at least one neighbouring pixel (8 neighbours are checked) which
    /// has value greater or equal to <see cref="HighThreshold"/>; otherwise it is none edge pixel. In the case
    /// if pixel's value is less than <see cref="LowThreshold"/>, then it is marked as none edge immediately.
    /// </note></para>
    /// 
    /// <para><note>The second difference is the <see cref="MaxGradient"/> property. In original algorithm 
    /// (see link) the maximum gradient is stored. In this implementation this cannot be done shader-based.
    /// So the value has to be set as property. In general the default value shows good results.
    /// </note></para>
    /// 
    /// <para>Sample usage:</para>
    /// <code>
    /// Bitmap image = new Bitmap("cathedral.jpg");
    /// 
    /// // 1. Offline rendering of images
    /// // create Processor, used as rendering framework
    /// Processor processor = new Processor();
    /// // starts Processor
    /// processor.Begin( image );
    /// // create CannyEdgeDetector filter
    /// CannyEdgeDetector filter = new CannyEdgeDetector( );
    /// processor.Filter = filter;
    /// // optional: configure filter
    /// ...
    /// // apply the filter, render to bitmap  
    /// Bitmap resultImage = processor.RenderToBitmap( );
    /// resultImage.Save( "CannyEdgeDetector.jpg", ImageFormat.Jpeg );
    /// // apply the filter, render to texture
    /// // Texture2D resultTexture = processor.RenderToTexture( );
    /// processor.End( );
    ///  
    /// // 2. Online rendering of videos or images
    /// // create any windows control for rendering in
    /// Form myForm = new Form
    /// {
    ///     Width = 1280,
    ///     Height = 720
    /// };
    /// // starts Processor
    /// processor.Begin( "Video.avi", myForm );
    /// // create CannyEdgeDetector filter
    /// filter = new CannyEdgeDetector( );
    /// processor.Filter = filter;
    /// // optional: configure filter
    /// ...
    /// // apply the filter
    /// myForm.Show( );
    /// while ( myForm.Created )
    /// {
    ///     processor.Render( );
    ///     Application.DoEvents( );
    /// }
    /// processor.End( );
    /// </code>
    /// 
    /// <para><b>Initial image:</b></para>
    /// <img src="../../Cathedral.jpg" width="500" height="338" />
    /// <para><b>Result image:</b></para>
    /// <img src="../../CannyEdgeDetector.jpg" width="500" height="338" />
    /// </remarks>
    /// 
    /// <seealso cref="Filter.NonSBIP.NSCannyEdgeDetector"/>
    /// 
    public sealed class CannyEdgeDetector : BaseFilter
    {
        // 462.750458f / 255 belongs to Cathedral.jpg
        private float maxGradient;
        private byte lowThreshold;
        private byte highThreshold;       

        #region effect properties
		/// <summary>If true, the filter results in an black-white image.</summary>
        public bool BlackWhite { get; set; }
        /// <summary>
        /// Defines the grayscale algorithm type. 
        /// See also <seealso cref="Grayscale.CommonAlgorithms"/>.
        /// </summary>
        public CommonAlgorithms GrayscaleAlgorithm { get; set; }

        /// <summary>The version of Gaussian blur filter. 
        /// Further informations: <see cref="FixedGaussianBlur.Versions"/></summary>
        public Versions Version { get; set; }

        /// <summary>
        /// Maximal gradient value, [0.1, 5.65].
        /// </summary>
        /// 
        /// <remarks><para>Gradient value for non-maximum-suppression calculation.</para>
        /// 
        /// <para>Default value is set to <b>1.8147</b>.</para>
        /// </remarks>
        /// 
        public float MaxGradient
        {
            get { return maxGradient; }
            set
            {
                // get new maxGradient, 5.65 is max possible gradient --> sqrt(4*4+4*4)
                maxGradient = (float)Math.Max(0.1, Math.Min(5.65, value));
            }
        }

        /// <summary>
        /// Low threshold for Canny algorithm.
        /// </summary>
        /// 
        /// <remarks><para>Low threshold value used for hysteresis
        /// (see  <a href="http://www.pages.drexel.edu/~weg22/can_tut.html">tutorial</a>
        /// for more information).</para>
        /// 
        /// <para>Default value is set to <b>20</b>.</para>
        /// </remarks>
        /// 
        public byte LowThreshold
        {
            get { return lowThreshold; }
            set
            {
                if (Effect == null)
                    throw new ArgumentException("LowThreshold" + InitMsg);
                // get new low threshold value
                lowThreshold = value;
                Effect.Parameters["LowThreshold"].SetValue(value);
            }
        }

        /// <summary>
        /// High threshold for Canny algorithm.
        /// </summary>
        /// 
        /// <remarks><para>High threshold value used for hysteresis
        /// (see  <a href="http://www.pages.drexel.edu/~weg22/can_tut.html">tutorial</a>
        /// for more information).</para>
        /// 
        /// <para>Default value is set to <b>40</b>.</para>
        /// </remarks>
        /// 
        public byte HighThreshold
        {
            get { return highThreshold; }
            set
            {
                if (Effect == null)
                    throw new ArgumentException("HighThreshold" + InitMsg);
                // get new high threshold value
                highThreshold = value;
                Effect.Parameters["HighThreshold"].SetValue(value);
            }
        }
        #endregion effect properties

        /// <summary>
        /// Initializes a new instance of the <see cref="CannyEdgeDetector"/> class.
        /// </summary>
        public CannyEdgeDetector() : base("CannyEdgeDetector")
        {
            // filter is not real-time capable
            RealtimeCapable = false;
        }

        private Texture2D GetGrayscaleTex()
        {
            Effect e = GetExternalEffect("Grayscale");
            switch (GrayscaleAlgorithm)
            {
                case CommonAlgorithms.BT709:
                    e.Parameters["Red"].SetValue(0.2125f);
                    e.Parameters["Green"].SetValue(0.7154f);
                    e.Parameters["Blue"].SetValue(0.0721f);
                    break;
                case CommonAlgorithms.RMY:
                    e.Parameters["Red"].SetValue(0.5f);
                    e.Parameters["Green"].SetValue(0.419f);
                    e.Parameters["Blue"].SetValue(0.081f);
                    break;
                case CommonAlgorithms.Y:
                    e.Parameters["Red"].SetValue(0.299f);
                    e.Parameters["Green"].SetValue(0.587f);
                    e.Parameters["Blue"].SetValue(0.114f);
                    break;
            }
            Texture2D grayscale = RenderExternalEffectToTexture(e, "Grayscale");
            return grayscale;
        }

        private Texture2D GetNonMaxSuppressTex()
        {
            // Step 0 - make image/texture grayscale
            Texture2D grayscale = GetGrayscaleTex();

            // STEP 1 - blur image
            Effect e = GetExternalEffect("FixedGaussianBlur");
            e.Parameters["Version"].SetValue((byte)Version);
            Texture2D blur = RenderExternalTextureWithExternalEffectToTexture(
                grayscale, e, "FixedGaussianBlur", SurfaceFormat.Color);

            // STEP 2-5 - calculate gradients (sobel operator), 
            //  finding edge direction, nonmaximum suppression
            e = GetExternalEffect("CannyNonmaximumsuppression");
            e.Parameters["MaxGradient"].SetValue(MaxGradient);
            Texture2D tex = RenderExternalTextureWithExternalEffectToTexture(
                blur, e, "CannyNonmaximumsuppression", SurfaceFormat.Color);
            return tex;
        }

        /// <summary>Renders the specified effect.</summary>
        internal override void RenderEffect()
        {
            Texture2D nonmaximumsuppression = GetNonMaxSuppressTex();
            // STEP 6 - hysteresis
            Effect.Parameters["ImageTexture"].SetValue(nonmaximumsuppression);
            Technique = BlackWhite ? "HysteresisBW" : "Hysteresis";
            base.RenderEffect();
        }

        /// <summary>Renders the specified effect into a texture.</summary>
        /// <returns>The rendered texture.</returns>
        internal override Texture2D RenderEffectToTexture()
        {
            Texture2D nonmaximumsuppression = GetNonMaxSuppressTex();
            // STEP 6 - hysteresis
            Technique = BlackWhite ? "HysteresisBW" : "Hysteresis";
            Texture2D hysteresis = RenderEffectWithExternalTextureToTexture(nonmaximumsuppression);
            return hysteresis;
        }

        /// <summary>
        /// Defines Laplace algorithm properties.
        /// </summary>
        protected override void PostInit()
        {
            MaxGradient = 1.8147f;
            LowThreshold = 20;
            HighThreshold = 40;
        }

        /// <summary>
        /// Checks graphics profile. CannyEdgeDetector needs XNA's HiDef 
        /// graphics profile.
        /// </summary>
        protected override void CheckHighDef()
        {
            if (!HiDef)
            {
                throw new NotImplementedException("Canny edge detector " + NotImplementedMsg);
            }
        }
    }
}
